﻿using DeepEarth.Map.Core.Filters;
using System;
using System.Linq;
using DeepEarth.Core;
using System.Collections.Generic;
using DeepEarth.Map.Core.Clustering;
using System.Windows.Threading;
using DeepEarth.Core.Data;
using System.Windows;
using DeepEarth.Core.Utilities;

#if WINDOWS_PHONE
// For tuple type
using DeepEarth.Core.Utilities.WP7;
#endif

namespace DeepEarth.Map.Core
{
    public class FilterProcessor
    {
        BoundsChecker boundsChecker;
        Clusterer clusterer;
        ChangeDetector changeDetector;
        PreRenderer prerenderer;
        Synchronizer synchronizer;

        bool backgroundProcessing;
        public bool IsProcessing
        {
            get { return backgroundProcessing; }
        }

        Guid syncToken;
        LocationRectangle syncRectangle;
        double syncViewportWidth;
        double syncActualWidth;
        
        object syncLock = new object();

        // used to lock any access to layerElements
        object layerElementsLock;

        Dictionary<ClusterViewModel, UIElement> layerElements;

        public FilterProcessor(Func<Location, Point> locationToViewportPoint, object collectionLock)
        {
            this.layerElementsLock = collectionLock;
            InitializeFilterChain(locationToViewportPoint);            
        }

        private void InitializeFilterChain(Func<Location, Point> locationToViewportPoint)
        {
            boundsChecker = new BoundsChecker();
            clusterer = new Clusterer();
            changeDetector = new ChangeDetector();
            prerenderer = new PreRenderer();
            synchronizer = new Synchronizer(locationToViewportPoint);
        }

        Dispatcher dispatcher;
        Dispatcher Dispatcher
        {
            get { return dispatcher; }
        }

        public void TriggerProcessing(LocationRectangle boundingRectangle, double currentViewportWidth, double mapPixelWidth, Dictionary<ClusterViewModel, UIElement> layerElements, Dispatcher dispatcher)
        {
            this.layerElements = layerElements;
            this.dispatcher = dispatcher;
            this.syncRectangle = boundingRectangle;
            this.syncViewportWidth = currentViewportWidth;
            this.syncActualWidth = mapPixelWidth;

            syncToken = Guid.NewGuid();
            RunBackground(new Guid());
        }

        void RunBackground(Guid token)
        {
            if (token != this.syncToken)
            {
                lock (syncLock)
                {
                    if (!backgroundProcessing)
                    {
                        backgroundProcessing = true;
                        System.Threading.ThreadPool.QueueUserWorkItem(BackgroundProcess_RemoveExistingOutOfBounds, this.syncToken);
                    }
                }
            }
        }

        void EndBackground(Guid token)
        {
            backgroundProcessing = false;
            RunBackground(token);
        }

        void BackgroundProcess_RemoveExistingOutOfBounds(object state)
        {
            // Remove existing items in view. Note that if we remove a cluster in view - and that cluster
            // includes an item which should be in view - we add that item back in the add phase.
            // Right now, we're just interested in removing stuff

            Guid processToken = (Guid)state;

            if (processToken != this.syncToken)
            {
                EndBackground(processToken);
                return;
            }

            //Background processing
            boundsChecker.ViewportRectangle = this.syncRectangle;
            double pixelWidth = this.syncActualWidth;
            double pixelsPerDegree = pixelWidth / this.syncRectangle.Width;
            clusterer.PixelsPerDegree = pixelsPerDegree;

            List<ClusterViewModel> currentlyVisible;
            lock (layerElementsLock)
            {
                currentlyVisible = this.layerElements.Keys.Where(r => r.IsVisible).ToList();
            }
            Dictionary<ClusterViewModel, ClusterViewModel> remainingList = boundsChecker.Filter(currentlyVisible);
            List<ClusterViewModel> outOfBounds = currentlyVisible.Where(f => !remainingList.ContainsKey(f)).ToList();

            Tuple<Guid, List<ClusterViewModel>> args = new Tuple<Guid, List<ClusterViewModel>>(processToken, remainingList.Keys.ToList());
            if (outOfBounds.Count > 0)
            {                
                Dispatcher.BeginInvoke(() =>
                {
                    PreRenderer.Remove(outOfBounds, this.layerElements);
                    System.Threading.ThreadPool.QueueUserWorkItem(BackgroundProcess_BoundsCheckAll, args);
                }
                );
            }
            else
            {
                BackgroundProcess_BoundsCheckAll(args);
            }
        }


        void BackgroundProcess_BoundsCheckAll(object state)
        {
            Tuple<Guid, List<ClusterViewModel>> stateTuple = (Tuple<Guid, List<ClusterViewModel>>)state;

            Guid processToken = stateTuple.Item1;
            List<ClusterViewModel> currentlyVisible = stateTuple.Item2;

            if (processToken != this.syncToken)
            {
                EndBackground(processToken);
                return;
            }

            //Background processing
            boundsChecker.ViewportRectangle = this.syncRectangle;
            double pixelWidth = this.syncActualWidth;
            double pixelsPerDegree = pixelWidth / this.syncRectangle.Width;
            clusterer.PixelsPerDegree = pixelsPerDegree;

            Dictionary<ClusterViewModel, ClusterViewModel> inBounds;

            lock (layerElementsLock)
            {
                inBounds = boundsChecker.Filter(this.layerElements.Keys.ToList());
            }

            if (processToken != this.syncToken)
            {
                EndBackground(processToken);
                return;
            }

            // for each of the existing clusters, remove all clustered elements that are no longer in view
            var existingClusters = currentlyVisible.Where(k => k.IsCluster).ToList();

            foreach (var cluster in existingClusters)
            {
                var removedChildren = cluster.GetClusteredItems().Where(c => !inBounds.ContainsKey(c)).ToList();

                foreach (var removed in removedChildren)
                {
                    cluster.RemoveFromCluster(removed);
                }
            }

            BackgroundProcess_Cluster(processToken, inBounds, currentlyVisible);
        }

        void BackgroundProcess_Cluster(Guid processToken, Dictionary<ClusterViewModel, ClusterViewModel> inBounds, List<ClusterViewModel> currentlyVisible)
        {
            Dictionary<ClusterViewModel, ClusterViewModel> clusters = clusterer.SimpleStreamCluster(inBounds, currentlyVisible.ToDictionary(k => k));

            // We're dealing with a list of references to the actual filtered items. In this function
            // those items will be mutated as items are clustered and there is a potential for a viewmodel to appear more than 
            // once in the global list of filtered items.
            // If the cluster has executed, then the filteredElements collection has been mutated. The changes must be committed or we will be in an 
            // inconsistent state!!!

            var changes = changeDetector.Filter(clusters, currentlyVisible);

            List<ClusterViewModel> removed = changes.Item1;
            List<ClusterViewModel> added = changes.Item2;

            Dispatcher.BeginInvoke(() =>
            {
                PreRenderer.Remove(removed, this.layerElements);

                Dispatcher.BeginInvoke(() =>
                {
                    PreRenderer.Add(added, this.layerElements);

                    List<ClusterViewModel> nowVisible;
                    lock (layerElementsLock)
                    {
                        nowVisible = this.layerElements.Keys.Where(k => k.IsVisible).ToList();
                    }

                    synchronizer.Filter(syncViewportWidth, nowVisible, this.layerElements);

                    clusters.Keys.Iter(c => c.Notify());

                    EndBackground(processToken);

                    Dispatcher.BeginInvoke(() =>
                    {
                        currentlyVisible.Iter(k => k.Notify());
                    });
                });
            });
        }
    }
}
